iT邦幫忙

2022 iThome 鐵人賽

DAY 25
0
自我挑戰組

JavaScript101與人生幹話系列 第 26

JavaScript101與人生幹話-函式、閉包、This與simple call

  • 分享至 

  • xImage
  •  

1函式

函式與物件的差別在有被呼叫的能力。
用function所宣告的字詞。

1-1具名函式能夠在函式內被調用

在函數陳述式的情況

function callMyName() {
    console.log('Jack')
}
function fn1() {
    callMyName()
}
fn1() // 'Jack'

1-2在函數表達式的情況

const callMyName = function myName() {
    console.log(callMyName, myName)
}

callMyName()
//ƒ myName() {
//    console.log(callMyName, myName)
//} ƒ myName() {
//    console.log(callMyName, myName)
//}

myName() // myName is not defined

2立即函式

如果在其他地方不需要呼叫到這個函式,只有在這個地方立刻使用。
以下示範例:

(function(){console.log('我執行了')}()); // '我執行了'
(function(){console.log('我執行了')})(); // '我執行了'

2-1立即函式的特點

立即函式有以下特點
1.立刻執行
2.無法在函式外執行,範例如下

(function IIFE(){console.log('我執行了')}()); // '我執行了'
IIFE() // IIFE is not defined

3.限制函式內變數的作用域,範例如下

(function (){
    var a = 'a'
    console.log(a)
})();
console.log('函式外',a) // ReferenceError: a is not defined

第二個範例

function countDown(seconde) {
    for(var i = 1 ; i<seconde + 1; i ++) {
        setTimeout(function(){console.log(i)}, i * 1000)
    }
}
countDown(3) // 4 4 4 每隔1秒出現
// 會出現上面的情況式因為var的作用域是整個for,所以i會先跑完for迴圈後最後以 i = 3 進入setTimeout

// 這個時候就可以使用立即函式限制i的作用域
function countDown(seconde) {
    for(var i = 1 ; i<seconde + 1; i ++) {
        (function(j){
           setTimeout(function(){console.log(j)}, i * 1000) 
        })(i)
    }
}
countDown(3)// 1 2 3 每隔1秒出現

2-1立即函式間傳遞變數

使用物件傳參考特性
範例一

const obj = {};
(function(data){
 var a = 'a'
 data.a = a
 console.log('立即函式1')
 })(obj); // '立即函式1'

(function(data){
    console.log(data.a)
})(obj); // 'a'
 

把全域變數傳入並附加屬性與值後,在第二個立值函式直接調用。
範例二

(function(global){
    var a = 'a'
    global.a = a
    console.log('立即函式1')
})(window); // '立即函式1'

(function(){
    console.log(a)
})(); // 'a'

3參數

3-1函式可以使用的參數

以下是函式可以使用的參數

var globalVariable = '全域'
function active(par) {
    var localVariable = '區域'
    console.log(par, arguments, globalVariable, localVariable, this)
}

active('我是參數', '5', true, 123)

以上範例結果如下圖

3-2函式內所宣告的變數

function callName(name){ // 其中的name等同於傳入的參數會被宣告為變數name
    var name // 由於已經宣告變數所以這行變得沒有意義。
    console.log(name)  // 'Jay'
    name = 'May'  // 需要直接賦值材能取代原來的參數
    console.log(name)  // 'May'
}
callName('Jay')

3-3函式內的Hoisting

function callName(name){ 
    
    console.log(name)  
    // ƒ name() {
    //console.log('我是function中的function')
    //  }
    function name() {
        console.log('我是function中的function')
    }
    name = 'May'  
    console.log(name)  // 'May'
}
callName('Jay')

結果如下圖

以下是步驟拆解

function callName(name){ 
    // 準備階段
    
    // 函數的參數比函式與變數更優先,優先度參數>函式>變數
    name = 'Jay' // 這理是舉例實際上應該不是這樣
    // 這個時候才輪到函式,然後就把name = 'Jay'蓋掉成function
    function name() {
        console.log('我是function中的function')
    }
    
    // 執行階段
    console.log(name)  
    // ƒ name() {
    //console.log('我是function中的function')
    //  }
    name = 'May'  
    console.log(name)  // 'May'
}
callName('Jay')

3-4參數只看從外面傳入的值與順序,與函式的參數命名無關。

function active(i, j, k) {
    console.log(i, j, k)
}
const a = 'a'
const b = 1

active(a, b) // 'a' 1 undefined

3-5參數是物件那依然有傳參考特性

var personal = {
    name:'Jack',
    age: 25,
    location:'Taipei'
}
function changeName(obj){
    obj.name = 'May'
}
changeName(personal)
console.log(personal.name) // 'May'

3-6callBack function

簡單來說就是把函式當作參數,傳入另一個函式內執行。

// 當成參數的函式可以是匿名函式
function active(fn){
    fn('參數') //傳入的函式也可以設定參數
}
active(function(pra){console.log(pra)}) // '參數'

// 當成參數的函式也可以式具名函式
function callpar(par) {
    console.log(par)
}
active(callpar) // '參數'

// 兩者的差別就在匿名函式需要把完整的函式寫入參數()內,而具名函式則只要寫入函式名稱。

3-7其它參數arguments

arguments適用在不確定會傳多少參數的函式。

function fn(a) {
    console.log(a,arguments)
}
fn(1, 'a', 5) // 1 Arguments(3) [1, 'a', 5, callee: ƒ, Symbol(Symbol.iterator): ƒ]

因為arguments是類陣列,只能使用部分的陣列功能,使用前可以轉成陣列。
範例如下

function fn(a) {
    // 用展開的方式轉為陣列
    const parments = [...arguments]
    parments.forEach((i) =>{
        console.log(i)
    })
}
fn(1, 'a', 5) // 1 'a' 5 

4閉包 Closure

4-1什麼是閉包?

閉包就是把函式內的變數,封在函式內。
以下是範例:

function walletMoney(){
    var money = 5000;
    return function(price){
        money = money - price
        return money
    }
}
walletMoney()(500) // 4500
// 使用函式表達式這樣子相當等於每一個變數都有自己的錢包。
var JayWallet = walletMoney()
console.log(JayWallet(1000)) // 4000

var MayWallet = walletMoney()
console.log(MayWallet(4500)) // 500

4-2工廠模式及私有方法

工廠模式
對4-1的範例進行修改

function walletMoney(initWalletMoney){
    var money = initWalletMoney;
    return function(price){
        money = money - price
        return money
    }
}
// 這樣子就可以自訂每個錢包的價格。
const myWallet = walletMoney(1000)
console.log(myWallet(700)) // 300
const MayWallet = alletMoney(5000)
console.log(MayWallet(1000)) // 4000

私有化方法
對工廠模式在進行修改

function walletMoney(initWalletMoney){
    // 如果沒有傳入金額則預設1000
    var money = initWalletMoney || 10000;
    return {
        spend(price){
            money = money - price
            return money
        },
        store(number){
            money = money + number
            return money
        },
        checkWallet(){
            return money
        }
    }
}
const myWallet = walletMoney(5000);
console.log(myWallet.checkWallet()) // 5000
console.log(myWallet.spend(500)) // 4500
console.log(myWallet.store(800)) // 5300

4this

4-1最常見的 this:物件的方法調用

this 的運作

4-2this:簡易呼叫

4-2-1什麼是simple call

simple call就是直接執行函式。
以下是範例:

var name = 'Jack'
function callName() {
    console.log(this.name)
}
callName() // Jack

以上的範例就是simple call。
特性是simple call的this都會指項全域。
盡可能的不要調用simple的this

4-2-2 simple call的種類

立即函式
var name = 'Jack'
;(function(){
    var name = 'May'
    console.log(this.name)
})() // 'Jack'
callback function

以下是範例

var name = 'Jack'
function callName(fn){
    const name = 'May'
    fn()
}
function call(){
    console.log(this.name)
}
callName(call) // 'Jack'

forEach也是

const arr = ['a', 'b', 'c']
var name = 'Jack'
function callName() {
    var name = 'May'
    arr.forEach(function(i){
    console.log(`${this.name} ${i}`)
  })
}
const data = {
    name:'May',
    callName
}
data.callName() // 'Jack a' 'Jack b' 'Jack c'
 

如何修改?

const arr = ['a', 'b', 'c']
var name = 'Jack'
function callName() {
    // 把call back function的外層的this指定成一個變數,
    // 並在claa back function取代this
    const that = this
    arr.forEach(function(i){
    console.log(`${that.name} ${i}`)
  })
}
const data = {
    name:'May',
    callName
}
data.callName()  // 'May a' 'May b' 'May c'

也可以這樣子改

const arr = ['a', 'b', 'c']
var name = 'Jack'
function callName() {
    // 或使用箭頭函式
    arr.forEach((i)=>{
    console.log(`${this.name} ${i}`)
  })
}
const data = {
    name:'May',
    callName
}
data.callName()// 'May a' 'May b' 'May c'

setTimeout的範例

var name = 'Jack'
function callName() {
    setTimeout(function(){
        console.log(this.name)
    }, 1000)
}
const data = {
    name : 'May',
    callName
}
data.callName() // 'Jack'

修改方法與forEach相同如下
修改一

var name = 'Jack'
function callName() {
    const that = this
    setTimeout(function(){
        console.log(that.name)
    }, 1000)
}
const data = {
    name : 'May',
    callName
}
data.callName() // 'May'

修改二

var name = 'Jack'
function callName() {
    setTimeout(()=>{
        console.log(this.name)
    }, 1000)
}
const data = {
    name : 'May',
    callName
}
data.callName() // 'May'
closure閉包

以下是範例:

var name = 'Jack'
function callName(){
    let name = 'May'
    return function(){
        console.log(this.name)
    }
}
const callSome = callName()
console.log(callSome()) // 'Jack'

可以從以上範例看到,this是指向全域中的name。
如何修改呢?

var name = 'Jack'
function callName(){
    return {
        name: 'May',
        call(){
            console.log(this.name)
        }
    }
}
const callSome = callName()
console.log(callSome.call()) // 'May'

最直接的方式就是不要使用simple call

4-3call, apply, bind 與 嚴謹模式

call, apply, bind是拿來定義this的指向與執行函式。

call, apply, bind的範例如下

var name = 'Jack'
var obj = {
    name: 'May'
}

function callName(par1, par2){
    console.log(this.name, par1, par2)
}

callName.call(obj, 1, 2) // 'May' 1 2
callName.apply(obj,[1,2]) // 'May' 1 2
// bind會回傳元函數所以要有變數來被賦值
const call = callName.bind(obj, 1, 2)
call() // 'May' 1 2
// bind也可以先導入部分的參數
const call2 = callName.bind(obj, 1,)
// call2很像simple call 但不是,他的this已經在bind的時候就決定了
call2() //  'May' 1 undefined
// 可以在執行的時候在加入
call2(2) // 'May' 1 2

4-3-1call, apply, bind傳入純值

以call為例子

function fn(par1, par2){
    console.log(this, par1, par2)
}
fn.call(1, 'par1', 'par2') // Number {1}[[Prototype]]: Number[[PrimitiveValue]]: 1 'par1' 'par2'
// 所傳入的純值會變成建構式,型別為物件
fn.call('1', 'par1', 'par2') // String {'1'} 'par1' 'par2'
fn.call(true, 'par1', 'par2') // Boolean {true} 'par1' 'par2'

//如果傳入null或是undefined則會是window
fn.call(null, 'par1', 'par2')
fn.call(undefined, 'par1', 'par2')
// Window {window: Window, self: Window, document: document, name: '', location: Location, …} 'par1' 'par2'

4-3-2call, apply, bind嚴格模式(strict monde)

在嚴格模式下可以避免一些奇怪的錯誤,例如在沒有定義變數的時候賦值變數,就會直接報錯。
以及對call, apply, bind做出一些調整。
如何使用?

// 如果在全域中使用則全部的程式碼都會變成嚴格模式
'use strict'
// 也可以在各別的function中使用
function fn(){
    'use strict'
    a = 'a'
}
fn() // Uncaught ReferenceError: a is not defined

以call為例在嚴格模式下的範例

'use strict'
function fn(par1, par2){
    console.log(this, par1, par2)
}
fn.call(1, 'par1', 'par2') //  1 'par1' 'par2'
// 所傳入的純值依舊是純值,
fn.call('1', 'par1', 'par2') // '1' 'par1' 'par2'
fn.call(true, 'par1', 'par2') // true 'par1' 'par2'

//如果傳入null或是undefined則會是window
fn.call(null, 'par1', 'par2') // null 'par1' 'par2'
fn.call(undefined, 'par1', 'par2') // undefined 'par1' 'par2'

在simple call的時候就像使用call()不傳入參數,所以simple call的本質是undefined

4-3DOM:this

在行內執行事件時,這個時候的this會指向正在進行這個事件的DOM。
範例如下

<button onclick="console.log(this.dataset.index,this)" data-index='0'>clickme</button>

點擊後會出現
<!-- 0 與 <button onclick="console.log(this.dataset.index,this)" data-index="0">clickme</button> -->

另外在addEventListener的時候也會指向DOM
範例如下

<ul>
  <li data-index='0'>1</li>
  <li data-index='1'>2</li>
  <li data-index='2'>3</li>
</ul>
.listStyle{
  background: red;
  color: white;
}

這裡才是重點

  const list = [...document.querySelectorAll('li')]
  function clickList() {
    list.forEach((j, index) => {
      console.log('this', this)
      if (Number(this.dataset.index) === index) {
        this.classList.toggle('listStyle')
      } else {
        j.classList.remove('listStyle')
      }
    })
  }
  list.forEach((i) => {
      // 可以注意到函式clickList內的this會指向i
    i.addEventListener('click', clickList)
      // 把直接寫入在function內,其中的this也是指向i
    i.addEventListener('click', function(){
        if (Number(this.dataset.index) === index) {
        this.classList.toggle('listStyle')
      } else {
        j.classList.remove('listStyle')
      }
    })  
  })

如果你這樣子寫this就不會指向DOM

 list.forEach((i) => {
    // 這樣子寫函式clickList的this指向全域,因為clickList()為simple call
    i.addEventListener('click', function(){clickList()})
  })

人生幹話 - 好隊友帶你上天堂

這個時候來了一位新同事,幫我分攤了許多工作,他是從法規的顧問公司跳槽過來,大約第二個月開始就重構QA的文件,尤其是那個魔王級的專案,順便連未來怎麼做的藍圖都已經完成,又經過半年的努力把工作流程都優化,並且完成大多數文件的法規進版,不得不說這位新人事真的強,我們在沒有上過新的法規課程的情況下,靠自己蒐集的資料完成最新法規的進版,被其他人認為有上過新的法規課程,那時時候武漢肺炎疫情的關係所有法規都暫停上課了。


上一篇
JavaScript101與人生幹話-比較詳細一點的物件說明
下一篇
JavaScript101與人生幹話-運算子、型別與文法
系列文
JavaScript101與人生幹話30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言